package cn.st4rlight.util.time;

import static java.time.format.DateTimeFormatter.ofPattern;

import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;

import com.google.common.collect.ImmutableMap;

/**
 * 有状态的时间类型，非线程安全
 *
 * @author st4rlight <st4rlight@163.com>
 * Created on 2024-05-04
 */
public class DateTimes implements Comparable<DateTimes> {

    public static final String MONTH_PATTERN = "yyyy-MM";
    public static final String DATE_PATTERN = "yyyy-MM-dd";
    public static final String DATE_PATTERN_NO_DASH = "yyyyMMdd";
    public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    public static final String ISO_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss:SSS";

    // immutable and thread-safe.
    public static final DateTimeFormatter DATE_FORMATTER = ofPattern(DATE_PATTERN);
    public static final DateTimeFormatter DATE_NO_DASH_FORMATTER = ofPattern(DATE_PATTERN_NO_DASH);
    public static final DateTimeFormatter DATE_TIME_FORMATTER = ofPattern(DATE_TIME_PATTERN);

    // pattern --> formatter
    public static final Map<String, DateTimeFormatter> FORMATTER_CACHE_MAP =
            ImmutableMap.<String, DateTimeFormatter>builder()
                    .put(DATE_PATTERN, DATE_FORMATTER)
                    .put(DATE_PATTERN_NO_DASH, DATE_NO_DASH_FORMATTER)
                    .put(DATE_TIME_PATTERN, DATE_TIME_FORMATTER)
                    .build();

    // 成员变量
    private LocalDateTime localDateTime;
    private DateTimeFormatter formatter;
    private ZoneId zoneId;


    /* ------------------------------- 构造函数 ------------------------------- */
    private DateTimes() {
        this(LocalDateTime.now());
    }

    private DateTimes(LocalDateTime localDateTime) {
        this(localDateTime, DATE_TIME_FORMATTER);
    }

    private DateTimes(LocalDateTime localDateTime, DateTimeFormatter formatter) {
        this(localDateTime, formatter, ZoneId.systemDefault());
    }

    private DateTimes(LocalDateTime localDateTime, DateTimeFormatter formatter, ZoneId zoneId) {
        this.localDateTime = localDateTime;
        this.formatter = formatter;
        this.zoneId = zoneId;
    }

    private DateTimes(DateTimes dateTimes) {
        this.localDateTime = dateTimes.localDateTime;
        this.formatter = dateTimes.formatter;
        this.zoneId = dateTimes.zoneId;
    }


    /* ------------------------------- 静态工厂函数 ------------------------------- */
    /**
     * 当前时间
     * 默认yyyy-MM-dd HH:mm:ss，默认当前系统时区
     */
    public static DateTimes now() {
        return new DateTimes();
    }

    public static DateTimes now(String pattern) {
        return now(pattern, ZoneId.systemDefault());
    }

    public static DateTimes now(String pattern, ZoneId zoneId) {
        return new DateTimes(LocalDateTime.now(), transToFormatter(pattern), zoneId);
    }

    public static DateTimes yesterday() {
        return now().plusDays(-1);
    }


    /* ------------------------------- 某一时间点 ------------------------------- */
    public static DateTimes from(LocalDateTime dateTime) {
        return from(dateTime, ISO_DATETIME_PATTERN);
    }

    public static DateTimes from(LocalDateTime dateTime, String pattern) {
        return from(dateTime, pattern, ZoneId.systemDefault());
    }

    public static DateTimes from(LocalDateTime dateTime, String pattern, ZoneId zoneId) {
        return new DateTimes(dateTime, DateTimeFormatter.ofPattern(pattern), zoneId);
    }

    public static DateTimes from(String dateTimeStr) {
        return from(dateTimeStr, DATE_TIME_PATTERN);
    }

    public static DateTimes from(String dateTimeStr, String pattern) {
        return from(dateTimeStr, pattern, ZoneId.systemDefault());
    }

    public static DateTimes from(String dateTimeStr, String pattern, ZoneId zoneId) {
        DateTimeFormatter tempFormatter = transToFormatter(pattern);
        LocalDateTime tempDateTime = LocalDateTime.parse(dateTimeStr, tempFormatter);
        tempDateTime = tempDateTime.atZone(zoneId).toLocalDateTime();
        return new DateTimes(tempDateTime, tempFormatter, zoneId);
    }

    public static DateTimes from(Long timestamp) {
        return from(timestamp, DATE_TIME_PATTERN);
    }

    public static DateTimes from(Long timestamp, String pattern) {
        return from(timestamp, pattern, ZoneId.systemDefault());
    }

    public static DateTimes from(Long timestamp, String pattern, ZoneId zoneId) {
        Instant instant = Instant.ofEpochMilli(timestamp);
        LocalDateTime tempDateTime = LocalDateTime.ofInstant(instant, zoneId);
        tempDateTime = tempDateTime.atZone(zoneId).toLocalDateTime();
        return new DateTimes(tempDateTime, transToFormatter(pattern), zoneId);
    }

    /* ------------------------------- fluent setter ------------------------------- */
    /**
     * 指定时间格式
     */
    public DateTimes formatter(String pattern) {
        this.formatter = transToFormatter(pattern);
        return from(this.localDateTime.format(this.formatter), pattern, this.zoneId);
    }

    /**
     * 指定时区
     */
    public DateTimes zoneId(ZoneId zoneId) {
        this.zoneId = zoneId;
        this.localDateTime = this.localDateTime.atZone(zoneId).toLocalDateTime();
        return this;
    }


    /* ------------------------------- fluent adjuster ------------------------------- */
    /**
     * 天，最小值
     */
    public DateTimes dayMin() {
        this.localDateTime = this.localDateTime.with(LocalTime.MIN);
        return this;
    }

    /**
     * 天，最大值
     */
    public DateTimes dayMax() {
        this.localDateTime = this.localDateTime.with(LocalTime.MAX);
        return this;
    }

    /**
     * 周初
     */
    public DateTimes firstDayOfWeek() {
        if (!DayOfWeek.MONDAY.equals(this.localDateTime.getDayOfWeek())) {
            this.localDateTime = this.localDateTime.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
        }
        return this;
    }

    /**
     * 月初
     */
    public DateTimes firstDayOfMonth() {
        this.localDateTime = this.localDateTime.with(TemporalAdjusters.firstDayOfMonth());
        return this;
    }

    /**
     * 月末
     */
    public DateTimes lastDayOfMonth() {
        this.localDateTime = this.localDateTime.with(TemporalAdjusters.lastDayOfMonth());
        return this;
    }

    /**
     * 年初
     */
    public DateTimes firstDayOfYear() {
        this.localDateTime = this.localDateTime.with(TemporalAdjusters.firstDayOfYear());
        return this;
    }

    /**
     * 年末
     */
    public DateTimes lastDayOfYear() {
        this.localDateTime = this.localDateTime.with(TemporalAdjusters.lastDayOfYear());
        return this;
    }


    /* ------------------------------- fluent computer ------------------------------- */
    public DateTimes plusSeconds(long seconds) {
        this.localDateTime = this.localDateTime.plusSeconds(seconds);
        return this;
    }

    public DateTimes minusSeconds(long seconds) {
        this.localDateTime = this.localDateTime.minusSeconds(seconds);
        return this;
    }

    public DateTimes plusMinutes(long minutes) {
        this.localDateTime = this.localDateTime.plusMinutes(minutes);
        return this;
    }

    public DateTimes minusMinutes(long minutes) {
        this.localDateTime = this.localDateTime.minusMinutes(minutes);
        return this;
    }

    public DateTimes plusHours(long hours) {
        this.localDateTime = this.localDateTime.plusHours(hours);
        return this;
    }

    public DateTimes minusHours(long hours) {
        this.localDateTime = this.localDateTime.minusHours(hours);
        return this;
    }

    public DateTimes plusDays(long days) {
        this.localDateTime = this.localDateTime.plusDays(days);
        return this;
    }

    public DateTimes minusDays(long days) {
        this.localDateTime = this.localDateTime.minusDays(days);
        return this;
    }

    public DateTimes plusWeeks(long weeks) {
        this.localDateTime = this.localDateTime.plusWeeks(weeks);
        return this;
    }

    public DateTimes minusWeeks(long weeks) {
        this.localDateTime = this.localDateTime.minusWeeks(weeks);
        return this;
    }

    public DateTimes plusMonths(long months) {
        this.localDateTime = this.localDateTime.plusMonths(months);
        return this;
    }

    public DateTimes minusMonths(long months) {
        this.localDateTime = this.localDateTime.minusMonths(months);
        return this;
    }

    public DateTimes plusYears(long years) {
        this.localDateTime = this.localDateTime.plusYears(years);
        return this;
    }

    public DateTimes minusYears(long years) {
        this.localDateTime = this.localDateTime.minusYears(years);
        return this;
    }

    /* ------------------------------- converter ------------------------------- */
    public LocalDateTime toLocalDateTime() {
        return this.localDateTime;
    }

    public LocalDate toLocalDate() {
        return this.localDateTime.toLocalDate();
    }

    public LocalTime toLocalTime() {
        return this.localDateTime.toLocalTime();
    }

    public Date toDate() {
        return Date.from(this.localDateTime.atZone(this.zoneId).toInstant());
    }

    @Override
    public String toString() {
        return this.localDateTime.format(this.formatter);
    }

    public Long toTimeStamp() {
        return this.localDateTime.atZone(this.zoneId).toInstant().toEpochMilli();
    }

    public Long toLong() {
        return this.toTimeStamp();
    }


    /* ------------------------------- checker ------------------------------- */
    /**
     * 判断是否是周末
     */
    public boolean isWeekend() {
        DayOfWeek dayOfWeek = this.localDateTime.getDayOfWeek();
        return DayOfWeek.SATURDAY.equals(dayOfWeek) || DayOfWeek.SUNDAY.equals(dayOfWeek);
    }

    /**
     * 判断是否是当天
     */
    public boolean isCurrDay() {
        LocalDate currDate = this.localDateTime.toLocalDate();
        LocalDate nowDay = LocalDate.now();
        return currDate.equals(nowDay);
    }

    /**
     * 比较时间差
     */
    public TimeDiff difference(DateTimes dateTimes) {
        Objects.requireNonNull(dateTimes);
        Period period = Period.between(this.localDateTime.toLocalDate(), dateTimes.localDateTime.toLocalDate());
        Duration duration = Duration.between(this.localDateTime, dateTimes.localDateTime);

        return new TimeDiff()
                .setYears(period.getYears())
                .setMonths(period.getMonths())
                .setDays(period.getDays())
                .setHours(duration.toHours())
                .setMinutes(duration.toMinutes())
                .setSeconds(duration.toSeconds())
                .setMillis(duration.toMillis())
                .setNanos(duration.toNanos());
    }

    /**
     * 检查一个字符串是否是合法的pattern格式
     */
    public static boolean isValidDateFormat(String timeStr, String pattern) {
        if (StringUtils.isBlank(timeStr)) {
            return false;
        }
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        sdf.setLenient(false); // 设置为非宽松模式，确保严格匹配格式
        try {
            sdf.parse(timeStr);
            return true; // 解析成功，字符串是合法的时间格式
        } catch (Exception ex) {
            return false; // 解析失败，字符串不是合法的时间格式
        }
    }

    /* ------------------------------- fluent comparator ------------------------------- */
    /**
     * pattern 转 formatter
     * 内部带缓存
     */
    public static DateTimeFormatter transToFormatter(String pattern) {
        return FORMATTER_CACHE_MAP.getOrDefault(pattern, DateTimeFormatter.ofPattern(pattern));
    }


    /* ------------------------------- fluent comparator ------------------------------- */
    /**
     * 日期比较
     * > 0: 大于比较日期
     * < 0: 小于比较日期
     * = 0: 相等
     */
    @Override
    public int compareTo(DateTimes dateTimes) {
        return this.localDateTime.compareTo(Objects.requireNonNull(dateTimes).localDateTime);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || this.getClass() != o.getClass()) {
            return false;
        }

        DateTimes dateTimes = (DateTimes) o;
        return Objects.equals(localDateTime, dateTimes.localDateTime)
                && Objects.equals(formatter, dateTimes.formatter)
                && Objects.equals(zoneId, dateTimes.zoneId);
    }

    @Override
    public int hashCode() {
        int result = Objects.hashCode(localDateTime);
        result = 31 * result + Objects.hashCode(formatter);
        result = 31 * result + Objects.hashCode(zoneId);
        return result;
    }
}
